AVL Tree
contents
자료구조의 다음 단계에 오신 것을 환영합니다. 이전 설명에서 우리는 일반적인 이진 탐색 트리(BST)가 가진 치명적인 결함을 확인했습니다: 이미 정렬된 데이터(1, 2, 3, 4, 5)를 삽입하면, 검색 속도가 느린 연결 리스트($O(n)$)로 퇴화해버린다는 점이었죠.
AVL 트리는 1962년 두 명의 소련 수학자 게오르기 아델슨-벨스키(Adelson-Velsky)와 예브게니 란디스(Landis)에 의해 발명되었습니다. 인류 역사상 최초로 만들어진 자가 균형 이진 탐색 트리(Self-Balancing Binary Search Tree) 입니다.
숫자를 삽입하거나 삭제할 때마다 AVL 트리는 스스로의 상태를 점검합니다. 만약 트리의 한쪽이 너무 "무거워지는" 것을 감지하면, 즉각적으로 수학적인 체조(이를 회전, Rotations이라고 부름)를 수행하여 다시 완벽한 균형 상태를 맞춥니다.
내부적으로 이것이 어떻게 작동하는지에 대한 상세한 분석입니다.
1. 핵심 개념: 균형 인수 (Balance Factor)
트리가 언제 균형을 맞춰야 할지 알기 위해, AVL 트리의 모든 노드는 균형 인수(Balance Factor, BF) 라는 특정한 숫자를 계산합니다.
$$\text{균형 인수} = (\text{왼쪽 서브트리의 높이}) - (\text{오른쪽 서브트리의 높이})$$
AVL 절대 규칙:
유효한 AVL 트리에서 _단 하나도 빠짐없이 모든 노드_의 균형 인수는 반드시 -1, 0, 1 중 하나여야 합니다.
0: 완벽하게 균형 잡힘.1: 왼쪽이 살짝 무거움 (허용됨).-1: 오른쪽이 살짝 무거움 (허용됨).
만약 단 하나의 노드라도 균형 인수가 2 또는 -2가 된다면, 경고벨이 울립니다. 트리가 공식적으로 불균형 상태가 된 것이며, 즉시 회전(Rotation)이 트리거됩니다.
2. 해결책: 트리 회전 (Tree Rotations)
트리가 불균형해지면, 노드들을 "회전"시켜서 스스로를 고칩니다. 시소를 생각하시면 됩니다. 만약 오른쪽이 너무 무거우면, 시소의 중간을 왼쪽으로 당겨 내려서 오른쪽을 위로 들어 올려야 합니다.
트리가 불균형해지는 시나리오는 정확히 4가지가 있으며, 이를 해결하는 회전 방식도 4가지입니다.
케이스 1: Left-Left (LL) 불균형 $\rightarrow$ 단일 우회전 (Single Right Rotation)
왼쪽 자식의 왼쪽 자식으로 노드를 삽입했을 때 발생합니다.
- 이전 상태: 루트가
3입니다.2를 삽입합니다(왼쪽으로 감).1을 삽입합니다(다시 왼쪽으로 감). 이제 루트인3의 균형 인수가+2가 됩니다. - 해결책: 가운데 노드인
2를 붙잡고 위로 끌어올려 새로운 루트로 만듭니다.3은 아래로 떨어져2의 오른쪽 자식이 됩니다. - 이후 상태:
2가 루트가 되고,1은 왼쪽,3은 오른쪽 자식이 됩니다. 완벽한 균형!
케이스 2: Right-Right (RR) 불균형 $\rightarrow$ 단일 좌회전 (Single Left Rotation)
오른쪽 자식의 오른쪽 자식으로 노드를 삽입했을 때 발생합니다.
- 이전 상태: 루트가
1입니다.2를 삽입(오른쪽).3을 삽입(오른쪽). 루트의 BF가-2가 됩니다. - 해결책:
2를 끌어올립니다.1은 아래로 떨어져 왼쪽 자식이 됩니다. - 이후 상태:
2가 루트,1이 왼쪽,3이 오른쪽.
케이스 3: Left-Right (LR) 불균형 $\rightarrow$ 이중 회전 (좌회전 후 우회전)
왼쪽 자식의 오른쪽 자식으로 노드를 삽입했을 때 발생합니다.
- 이전 상태: 루트가
3입니다.1을 삽입(왼쪽).2를 삽입(1의 오른쪽). 모양이>형태의 지그재그가 됩니다. - 해결책:
3을 곧바로 회전시킬 수 없습니다.- 먼저 맨 아래 두 노드(
1과2)에 대해 좌회전을 수행하여 지그재그를 일직선(3$\rightarrow$2$\rightarrow$1)으로 폅니다. - 이제 "LL" 케이스가 되었습니다! 루트인
3에 대해 우회전을 수행합니다.
- 먼저 맨 아래 두 노드(
케이스 4: Right-Left (RL) 불균형 $\rightarrow$ 이중 회전 (우회전 후 좌회전)
오른쪽 자식의 왼쪽 자식으로 노드를 삽입했을 때 발생합니다.
- 이전 상태: 루트가
1입니다.3을 삽입(오른쪽).2를 삽입(3의 왼쪽). 모양이<형태의 지그재그가 됩니다. - 해결책: 1. 먼저
3과2에 대해 우회전을 하여 일직선으로 만듭니다. 2. 그다음 루트인1에 대해 좌회전을 수행합니다.
3. 시간 복잡도: 엄격한 규칙이 주는 이점
AVL 트리는 완벽한 균형을 유지하는 데 집착하기 때문에, 트리의 높이가 수학적으로 항상 $O(\log n)$으로 보장됩니다.
| 연산 | AVL 트리 | 일반 BST (최악의 경우) |
|---|---|---|
| 탐색 (Search) | $O(\log n)$ | $O(n)$ |
| 삽입 (Insert) | $O(\log n)$ | $O(n)$ |
| 삭제 (Delete) | $O(\log n)$ | $O(n)$ |
AVL 트리에 10억 개의 데이터가 있다고 해도, 특정 데이터를 찾는 데 최대 30단계면 충분합니다. 반면 퇴화된 일반 BST에서는 10억 단계가 걸릴 수 있습니다.
4. AVL 트리 vs. 레드-블랙 트리 (업계 표준)
AVL 트리가 이렇게 완벽하다면, 왜 Java(HashMap, TreeMap)와 C++(std::map)은 대신 레드-블랙 트리(Red-Black Tree) 를 사용할까요?
완벽함에는 대가가 따르기 때문입니다.
- AVL 트리는 엄격하게 균형을 맞춥니다. 이로 인해 탐색(데이터 읽기)에 있어서는 절대적으로 가장 빠른 트리입니다. 하지만 너무 엄격하기 때문에, 삽입과 삭제를 할 때마다 회전(Rotation) 작업을 아주 빈번하게 수행해야 합니다.
- 레드-블랙 트리는 느슨하게 균형을 맞춥니다. 한쪽이 다른 쪽보다 최대 두 배까지 길어지는 것을 허용합니다. 조건이 덜 빡빡하기 때문에 회전 작업을 덜 자주 합니다.
경험적 법칙 (Rule of Thumb):
- 애플리케이션이 읽기(탐색) 작업을 많이 하고 쓰기 작업은 거의 없다면: AVL 트리를 사용하세요 (예: 캐싱이 많이 되는 데이터베이스 인덱스).
- 애플리케이션이 쓰기(삽입/삭제) 작업을 많이 한다면: 레드-블랙 트리를 사용하세요 (예: 프로그래밍 언어의 기본 라이브러리 자료구조).
references